Completed
Pull Request — master (#104)
by MusikAnimal
02:18
created

editcounter.js ➔ $   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
dl 0
loc 82
rs 8.5076
c 3
b 0
f 1
cc 4
nc 5
nop 0

4 Functions

Rating   Name   Duplication   Size   Complexity  
A editcounter.js ➔ ... ➔ $.fail 0 5 1
A editcounter.js ➔ ... ➔ $(ꞌ.chart-wrapperꞌ).each 0 20 2
A editcounter.js ➔ ... ➔ $.done 0 3 1
A editcounter.js ➔ ... ➔ setupToggleTable 0 12 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
$(function () {
2
    // Don't do anything if this isn't a Edit Counter page.
3
    if ($("body.ec").length === 0) {
4
        return;
5
    }
6
7
    // Set up charts.
8
    $(".chart-wrapper").each(function () {
9
        var chartType = $(this).data("chart-type");
10
        if ( chartType === undefined ) {
11
            return false;
12
        }
13
        var data = $(this).data("chart-data");
14
        var labels = $(this).data("chart-labels");
15
        var $ctx = $("canvas", $(this));
16
17
        /** global: Chart */
18
        new Chart($ctx, {
0 ignored issues
show
Unused Code Best Practice introduced by
The object created with new Chart($ctx, {Identif...e))))),false,false)))}) is not used but discarded. Consider invoking another function instead of a constructor if you are doing this purely for side effects.
Loading history...
19
            type: chartType,
20
            data: {
21
                labels: labels,
22
                datasets: [ { data: data } ]
23
            }
24
        });
25
26
        return undefined;
27
    });
28
29
    // Load top edits HTML via AJAX, to not slow down the initial page load.
30
    // Only load if container is present, which is missing in subroutes, e.g. ec-namespacetotals, etc.
31
    var $topEditsContainer = $("#topedits-container");
32
    if ($topEditsContainer[0]) {
33
        /** global: xtBaseUrl */
34
        var url = xtBaseUrl + 'topedits/'
35
            + $topEditsContainer.data('project') + '/'
36
            + $topEditsContainer.data('username') + '/all?htmlonly=yes';
37
        $.ajax({
38
            url: url,
39
            timeout: 30000
40
        }).done(function (data) {
41
            $topEditsContainer.replaceWith(data);
42
        }).fail(function (_xhr, _status, message) {
43
            $topEditsContainer.replaceWith(
44
                $.i18n('api-error', 'TopEdits API: <code>' + message + '</code>')
45
            );
46
        });
47
    }
48
49
    // Load recent global edits' HTML via AJAX, to not slow down the initial page load.
50
    // Only load if container is present, which is missing in subroutes, e.g. ec-namespacetotals, etc.
51
    var $latestGlobalContainer = $("#latestglobal-container");
52
    if ($latestGlobalContainer[0]) {
53
        /** global: xtBaseUrl */
54
        var url = xtBaseUrl + 'ec-latestglobal/'
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable url already seems to be declared on line 34. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
55
            + $latestGlobalContainer.data('project') + '/'
56
            + $latestGlobalContainer.data('username') + '?htmlonly=yes';
57
        $.ajax({
58
            url: url,
59
            timeout: 30000
60
        }).done(function (data) {
61
            $latestGlobalContainer.replaceWith(data);
62
        }).fail(function (_xhr, _status, message) {
63
            $latestGlobalContainer.replaceWith(
64
                $.i18n('api-error', 'Global contributions API: <code>' + message + '</code>')
65
            );
66
        });
67
    }
68
69
    // Set up namespace toggle chart.
70
    setupToggleTable(window.namespaceTotals, window.namespaceChart, null, function (newData) {
71
        var total = 0;
72
        Object.keys(newData).forEach(function (namespace) {
73
            total += parseInt(newData[namespace], 10);
74
        });
75
        var namespaceCount = Object.keys(newData).length;
76
        $('.namespaces--namespaces').text(
77
            namespaceCount.toLocaleString() + " " +
78
            $.i18n('num-namespaces', namespaceCount)
79
        );
80
        $('.namespaces--count').text(total.toLocaleString());
81
    });
82
});
83
84
/**
85
 * Set up the monthcounts or yearcounts chart.
86
 * @param {String} id 'year' or 'month'.
87
 * @param {Array} datasets Datasets grouped by mainspace.
88
 * @param {Array} labels The bare labels for the y-axis (years or months).
89
 * @param {Number} maxTotal Maximum value of year/month totals.
90
 */
91
window.setupMonthYearChart = function (id, datasets, labels, maxTotal) {
92
    /**
93
     * Namespaces that have been excluded from view via clickable
94
     * labels above the chart.
95
     * @type {Array}
96
     */
97
    var excludedNamespaces = [];
98
99
    /**
100
     * Number of digits of the max month/year total. We want to keep this consistent
101
     * for aesthetic reasons, even if the updated totals are fewer digits in size.
102
     * @type {Number}
103
     */
104
    var maxDigits = maxTotal.toString().length;
105
106
    /** @type {Array} Labels for each namespace. */
107
    var namespaces = datasets.map(function (dataset) {
108
        return dataset.label;
109
    });
110
111
    /**
112
     * Build the labels for the y-axis of the year/monthcount charts,
113
     * which include the year/month and the total number of edits across
114
     * all namespaces in that year/month.
115
     */
116
    function getYAxisLabels()
117
    {
118
        var labelsAndTotals = {};
119
        datasets.forEach(function (namespace) {
120
            if (excludedNamespaces.indexOf(namespace.label) !== -1) {
121
                return;
122
            }
123
124
            namespace.data.forEach(function (count, index) {
125
                if (!labelsAndTotals[labels[index]]) {
126
                    labelsAndTotals[labels[index]] = 0;
127
                }
128
                labelsAndTotals[labels[index]] += count;
129
            });
130
        });
131
132
        // Format labels with totals next to them. This is a bit hacky,
133
        // but it works! We use tabs (\t) to make the labels/totals
134
        // for each namespace line up perfectly.
135
        // The caveat is that we can't localize the numbers because
136
        // the commas are not monospaced :(
137
        return Object.keys(labelsAndTotals).map(function (year) {
138
            var digitCount = labelsAndTotals[year].toString().length;
139
            var numTabs = (maxDigits - digitCount) * 2;
140
141
            // +5 for a bit of extra spacing.
142
            return year + Array(numTabs + 5).join("\t") +
143
                labelsAndTotals[year];
144
        });
145
    }
146
147
    window[id + 'countsChart'] = new Chart($('#' + id + 'counts-canvas'), {
148
        type: 'horizontalBar',
149
        data: {
150
            labels: getYAxisLabels(),
151
            datasets: datasets
152
        },
153
        options: {
154
            tooltips: {
155
                intersect: true,
156
                callbacks: {
157
                    label: function (tooltip) {
158
                        return tooltip.xLabel.toLocaleString();
159
                    },
160
                    title: function (tooltip) {
161
                        var yLabel = tooltip[0].yLabel.replace(/\t.*/, '');
162
                        return yLabel + ' - ' + namespaces[tooltip[0].datasetIndex];
163
                    }
164
                }
165
            },
166
            responsive: true,
167
            maintainAspectRatio: false,
168
            scales: {
169
                xAxes: [{
170
                    stacked: true,
171
                    ticks: {
172
                        beginAtZero: true,
173
                        callback: function (value) {
174
                            if (Math.floor(value) === value) {
175
                                return value.toLocaleString();
176
                            }
177
                        }
178
                    }
179
                }],
180
                yAxes: [{
181
                    stacked: true
182
                }]
183
            },
184
            legend: {
185
                // Happens when the user enables/disables a namespace via the
186
                // labels above the chart.
187
                onClick: function (e, legendItem) {
188
                    // Update totals, skipping over namespaces that have been excluded.
189
                    if (legendItem.hidden) {
190
                        excludedNamespaces = excludedNamespaces.filter(function (namespace) {
191
                            return namespace !== legendItem.text;
192
                        });
193
                    } else {
194
                        excludedNamespaces.push(legendItem.text);
195
                    }
196
197
                    // Update labels with the new totals.
198
                    window[id + 'countsChart'].config.data.labels = getYAxisLabels();
199
200
                    // Yield to default onClick event, which re-renders the chart.
201
                    Chart.defaults.global.legend.onClick.call(this, e, legendItem);
202
                }
203
            }
204
        }
205
    });
206
}
207